package raytracing;
import util.Matrix4f;
import util.Point2f;
import util.Vector3f;

/**
 * A Camera used for raytracing
 * @author glenn
 *
 */
public class Camera {
	
	/**
	 * Vectors describing the camera
	 */
	private Vector3f eye, lookat, up, u, v, w, gaze;
	private float l,r,b,t,d;
	private int nx, ny;
	private String name;
	
	/**
	 * The default amount of horizontal pixels in the image
	 */
	public static final int DEFAULT_NX = 512;
	
	/**
	 * The default amount of vertical pixels in the image
	 */
	public static final int DEFAULT_NY = 512;
	
	/**
	 * Constructs a camera, given the eye and lookat points and up vector.
	 * Lookat and (lookat-eye) should be perpendicular.
	 * The fov parameter determines the width (angle) of the field of view.
	 * The default values for nx and ny will be used (DEFAULT_NX and DEFAULT_NY).
	 * @param eye the point of the camera in the space
	 * @param lookat the point the camera is looking at
	 * @param up the vector pointing upward, looking through the camera
	 * @param pov the angle of the field of view, in degrees
	 */
	public Camera(Vector3f eye, Vector3f lookat, Vector3f up,
			float fov, String name){
		this(eye,lookat,up,fov,DEFAULT_NX, DEFAULT_NY, name);
	}
	
	/**
	 * Constructs a camera, given the eye and lookat points and up vector.
	 * Lookat and (lookat-eye) should be perpendicular.
	 * The fov parameter determines the width (angle) of the field of view.
	 * nx and ny determine the amount of pixels (horizontal and vertical) in the image.
	 * @param eye the point of the camera in the space
	 * @param lookat the point the camera is looking at
	 * @param up the vector pointing upward, looking through the camera
	 * @param pov the angle of the field of view, in degrees
	 * @param nx the amount of pixels horizontally
	 * @param ny the amount of pixels vertically
	 */
	public Camera(Vector3f eye, Vector3f lookat, Vector3f up,
			float fov, int nx, int ny, String name){
		float edge = (float)(Math.tan(Math.toRadians(fov / 2)));
		this.eye = eye;
		this.lookat = lookat;
		this.up = up;
		this.l = -edge;
		this.r = edge;
		this.b = -edge;
		this.t = edge;
		this.nx= nx;
		this.ny= ny;
		this.d = 1;
		this.name = name;
		calcVectors();
	}
	
	/**
	 * Constructs a camera, given the eye and lookat points and up vector.
	 * Lookat and (lookat-eye) should be perpendicular.
	 * The l and r parameters determine the width (left and right edge) of the "Field of View".
	 * The b and t parameters determine the height (top and bottom edge) of the "Field of View".
	 * l and r are positioned along the u axis.
	 * b and t are positioned along the v axis.
	 * nx and ny determine the amount of pixels in the field of view (=image).
	 * d determines the focal length (or image-plane distance).
	 * @param eye the point of the camera in the space
	 * @param lookat the point the camera is looking at
	 * @param up the vector pointing upward, looking through the camera
	 * @param l the left edge of the field of view
	 * @param r the right edge of the field of view
	 * @param b the bottom edge of the field of view
	 * @param t the top edge of the field of view
	 * @param nx the amount of pixels horizontally
	 * @param ny the amount of pixels vertically
	 * @param d the focal length of the camera
	 * @param name the name of the camera
	 */
	public Camera(Vector3f eye, Vector3f lookat, Vector3f up,
			float l, float r, float b, float t, int nx, int ny, float d, String name){
		this.eye = eye;
		this.lookat = lookat;
		this.up = up;
		this.l = l;
		this.r = r;
		this.b = b;
		this.t = t;
		this.nx=nx;
		this.ny=ny;
		this.d = d;
		this.name = name;
		calcVectors();
	}
	
	/**
	 * Returns the left edge of the image, along the u axis
	 * @return l the left edge of the image, along the u axis
	 */
	public float getL() {
		return l;
	}
	
	/**
	 * Sets l, the left edge of the image, along the u axis
	 * @param l the left edge of the image, along the u axis
	 */
	public void setL(float l) {
		this.l = l;
	}

	/**
	 * Returns r, the right edge of the image, along the u axis
	 * @return r the right edge of the image, along the u axis
	 */
	public float getR() {
		return r;
	}
	
	/**
	 * Sets r, the right edge of the image, along the u axis
	 * @param r the right edge of the image, along the u axis
	 */
	public void setR(float r) {
		this.r = r;
	}

	/**
	 * Returns b, the bottom edge of the image, along the v axis
	 * @return b the bottom edge of the image, along the v axis
	 */
	public float getB() {
		return b;
	}
	
	/**
	 * Sets b, the bottom edge of the image, along the v axis
	 * @param b the bottom edge of the image, along the v axis
	 */
	public void setB(float b) {
		this.b = b;
	}
	
	/**
	 * Returns t, the bottom edge of the image, along the v axis
	 * @return t the bottom edge of the image, along the v axis
	 */
	public float getT() {
		return t;
	}
	
	/**
	 * Sets t, the bottom edge of the image, along the v axis
	 * @param t the bottom edge of the image, along the v axis
	 */
	public void setT(float t) {
		this.t = t;
	}

	/**
	 * Return nx, the number of pixels horizontally in the image.
	 * @return nx, the number of pixels horizontally in the image.
	 */
	public int getNx() {
		return nx;
	}
	
	/**
	 * Sets nx, the number of pixels horizontally in the image.
	 * @param nx, the number of pixels horizontally in the image.
	 */
	public void setNx(int nx) {
		this.nx = nx;
	}
	
	/**
	 * Returns ny, the number of pixels vertically in the image.
	 * @return ny, the number of pixels vertically in the image.
	 */
	public int getNy() {
		return ny;
	}
	
	/**
	 * Sets ny, the number of pixels vertically in the image.
	 * @param ny, the number of pixels vertically in the image.
	 */
	public void setNy(int ny) {
		this.ny = ny;
	}

	/**
	 * Returns the vector pointing rightward, looking through the camera
	 * @return u the vector pointing rightward, looking through the camera
	 */
	public Vector3f getU(){
		return up.cross(getW()).normalize();
	}
	
	public void setEye(Vector3f eye){
		this.eye = eye;
		calcVectors();
	}
	
	private void calcVectors() {
		this.gaze = lookat.subtract(eye).normalize();
		this.w = getGaze().scale(-1);
		this.u = up.cross(getW()).normalize();
		this.v = getW().cross(getU()).normalize();
	}

	public void setLookat(Vector3f lookat){
		this.lookat = lookat;
		calcVectors();
	}
	
	/**
	 * Returns the v vector of the camera coordinate system.
	 * @return v the v vector of the camera coordinate system.
	 */
	public Vector3f getV(){
		return v;
	}

	
	/**
	 * Returns the w vector of the camera coordinate system.
	 * This is the vector pointing backwards, looking through the camera.
	 * @return the w vector of the camera coordinate system.
	 */
	public Vector3f getW() {
		return w;
	}
	
	/**
	 * Returns the gaze vector of the camera.
	 * This is the vector from the eye point to the lookat point.
	 * @return
	 */
	public Vector3f getGaze() {
		return gaze;
	}

	/**
	 * Returns the vector pointing upward, looking through the camera
	 * @return v the vector pointing upward, looking through the camera
	 * @return
	 */
	public Vector3f getUp(){
		return up;
	}
	
	/**
	 * Returns the lookat point of this camera
	 * @return the lookat point of this camera
	 */
	public Vector3f getLookat(){
		return lookat;
	}

	/**
	 * Returns the point of the camera in space
	 * @return the eye, the point of the camera in space
	 */
	public Vector3f getEye(){
		return eye;
	}
	
	/**
	 * @return the focal length
	 */
	public float getD() {
		return d;
	}

	/**
	 * @param d the focal length to set;
	 */
	public void setD(float d) {
		this.d = d;
	}
	
	/**
	 * @return the name of the camera
	 */
	public String getName() {
		return name;
	}

	/**
	 * @param d the name of the camera to set;
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Generates a orthogonal ray
	 * @param i the horizontal position of the pixel (in the image) this ray shoots through
	 * @param j the vertical position of the pixel (in the image) this ray shoots through
	 * @return an orthogonal ray generated by this camera, going through the pixel at (i,j)
	 */
	public Ray generateOrthogonalRay(int i, int j){
		float u = l + (r-l) * (i + 0.5f)/nx;
		float v = b + (t-b) * (j + 0.5f)/ny;
		Vector3f direction = getW().scale(-1); //direction = -w
		Vector3f origin = getEye().add(getU().scale(u)).add(getV().scale(v)); //origin = e + uU + vV
		return new Ray(origin, origin.add(direction));
	}
	
	/**
	 * Generates a perspective ray
	 * @param i the horizontal position of the pixel (in the image) this ray shoots through
	 * @param j the vertical position of the pixel (in the image) this ray shoots through
	 * @return a perspective ray generated by this camera, going through the pixel at (i,j)
	 */
	public Ray generatePerspectiveRay(float i, float j){
		float u = l + (r-l) * (i)/nx;
		float v = b + (t-b) * (j)/ny;
		Vector3f direction = getW().scale(-d).add(getU().scale(u)).add(getV().scale(v));
		Vector3f origin = getEye();
		return new Ray(origin, origin.add(direction));
	}
	
	/**
	 * Generates a motion blurred perspective ray
	 * The eye points are jittered over a square lens
	 * @param i
	 * @param j
	 * @param length
	 * @param sample A jitter sample in range [-1,1]
	 * @return
	 */
	public Ray generateDoFPerspectiveRay(float i, float j, float length, Point2f sample){
		float u = l + (r-l) * (i)/nx,
		      v = b + (t-b) * (j)/ny,
		      radius = length / 2;
		Vector3f direction = getW().scale(-d).add(getU().scale(u)).add(getV().scale(v));
		Vector3f origin = getEye();
		//Jitter origin
		origin = origin.add(getU().scale(radius * sample.x)).add(getV().scale(radius * sample.y));
		return new Ray(origin, origin.add(direction));
	}
	
	public Matrix4f getCameraMatrix(){
		return Matrix4f.createCameraMatrix(eye, lookat, up);
	}
	
	public Matrix4f getViewportMatrix(){
		return Matrix4f.createViewPortMatrix(nx, ny);
	}
	
	public Matrix4f getOrthogonalMatrix(float n, float f){
		return Matrix4f.createOrthographicProjectionMatrix(l, r, t, b, n, f);
	}
	
	public Matrix4f getPerspectiveMatrix(float n, float f){
		return Matrix4f.createPerspectiveMatrix(n, f);
	}
	
	public Matrix4f getMvpMorthP(float n, float f){
		Matrix4f mvp = getViewportMatrix(),
				 morth = getOrthogonalMatrix(n, f),
				 p = getPerspectiveMatrix(n, f);
		return mvp.multiply(morth.multiply(p));
	}
	
	public Matrix4f getFullMatrix(float n, float f){
		Matrix4f mvp = getViewportMatrix(),
				 morth = getOrthogonalMatrix(n, f),
				 p = getPerspectiveMatrix(n, f),
				 mcam = getCameraMatrix();
		return mvp.multiply(morth.multiply(p.multiply(mcam)));
	}
}
